// src/services/BackupService.js
/**
 * 备份服务模块
 * 用于实现数据库的完整备份与增量备份功能
 * 支持备份文件加密、分片处理大数据量和自动清理过期备份
 */

// 导入必要的依赖库
const mysqldump = require('mysqldump'); // MySQL数据库备份工具
const fs = require('fs-extra');         // 增强版文件系统操作库
const path = require('path');           // 路径处理工具
const moment = require('moment-timezone'); // 日期时间处理库(带时区)
const archiver = require('archiver');   // 文件压缩工具
const crypto = require('crypto');       // 加密功能库
const prisma = require('../config/database'); // Prisma ORM客户端
const formatDateTime = require('../utils/formatDateTime');
require('dotenv').config();            // 加载环境变量配置

// 设置moment默认时区为东八区（中国标准时间）
moment.tz.setDefault('Asia/Shanghai');

/**
 * 获取东八区（中国标准时间）的当前时间
 * @returns {Date} 东八区的当前时间
 */
function getChinaTime() {
  return new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
}

/**
 * 备份服务类
 * 处理系统数据的备份和还原相关功能
 */
class BackupService {
  /**
   * 构造函数
   * 初始化备份目录结构和配置参数
   */
  constructor() {
    // 创建备份目录结构
    this.backupDir = path.join(process.cwd(), 'backups'); // 主备份目录
    fs.ensureDirSync(this.backupDir); // 确保主备份目录存在
    
    // 创建各类型备份子目录
    this.fullBackupDir = path.join(this.backupDir, 'full'); // 全量备份目录
    this.incrementalBackupDir = path.join(this.backupDir, 'incremental'); // 增量备份目录
    this.differentialBackupDir = path.join(this.backupDir, 'differential'); // 差异备份目录
    
    // 确保各子目录存在
    fs.ensureDirSync(this.fullBackupDir);
    fs.ensureDirSync(this.incrementalBackupDir);
    fs.ensureDirSync(this.differentialBackupDir);
    
    // 备份和压缩选项
    this.defaultCompressionLevel = 9; // 最高压缩比，最慢但体积最小，适用于存储空间敏感场景（如备份）
    this.defaultEncryptionAlgorithm = 'aes-256-cbc'; // 默认加密算法，AES-256-CBC是一种强加密标准
    this.sliceSize = 5000; // 每个分片包含的试卷数量，防止单个文件过大
    this.isZipEncryptedRegistered = false; // 用于跟踪是否已注册zip-encrypted格式
  }
  
  /**
   * 获取最近成功的全量备份时间
   * 用于差异备份和增量备份的起始时间点确定
   * @returns {Date|null} 最近成功全量备份的时间，如果不存在则返回null
   */
  async getLastFullBackupTime() {
    // 查询最近一次成功的全量备份记录
    const lastFullBackup = await prisma.backup_record.findFirst({
      where: {
        backup_type: 'full', // 只查找全量备份
        status: 'success'    // 且备份状态为成功的
      },
      orderBy: {
        created_at: 'desc'   // 按创建时间倒序排列，获取最新的
      }
    });
    
    // 返回备份时间，如果不存在则返回null
    return lastFullBackup ? lastFullBackup.created_at : null;
  }
  
  /**
   * 获取增量备份的时间范围
   * 确定本次增量备份应该涵盖的数据变更时间段
   * @returns {Object} 包含起始时间和结束时间的对象
   */
  async getIncrementalTimeRange() {
    // 找到最近的成功备份（无论类型）作为增量备份的起点
    const lastBackup = await prisma.backup_record.findFirst({
      where: {
        status: 'success' // 任何类型的成功备份
      },
      orderBy: {
        created_at: 'desc' // 按创建时间倒序排列，获取最新的
      }
    });
    
    // 设置时间范围：从上次备份时间到当前时间
    const startTimeDate = lastBackup ? lastBackup.created_at : null;
    // 格式化开始时间（用于SQL查询和显示）
    const startTime = startTimeDate ? formatDateTime(startTimeDate) : null;

   // 获取当前时间Date对象（用于Prisma查询）
    const now = new Date();
    const endTimeDate = new Date(now.getTime() + (8 * 60 * 60 * 1000));
    
    // 格式化结束时间（用于SQL查询和显示）
    const endTime = formatDateTime(endTimeDate);

    return {     
      startTime,    // 格式化的字符串
      endTime,      // 格式化的字符串
      startTimeDate, // 原始日期对象
      endTimeDate   // 原始日期对象 };
  }
}
    
  /**
   * 创建带密码的加密压缩文件
   * 将源文件或目录压缩并加密，增强备份数据安全性
   * @param {string} sourcePath 源文件或目录路径
   * @param {string} destPath 目标压缩文件路径
   * @param {string} password 加密密码
   * @returns {Promise<Object>} 返回包含路径、大小和IV的对象
   */
  async createEncryptedArchive(sourcePath, destPath, password) {
    return new Promise((resolve, reject) => {
      try {
        // 导入所需模块
        const archiver = require('archiver');
        const fs = require('fs-extra');
        
        // 注册加密格式（全局只需注册一次）
        if (!this.isZipEncryptedRegistered) {
          archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
          this.isZipEncryptedRegistered = true;
        }
        
        // 创建输出流
        const output = fs.createWriteStream(destPath);
        
        // 创建支持密码保护的ZIP归档
        const archive = archiver.create('zip-encrypted', {
          zlib: { level: this.defaultCompressionLevel }, // 使用最高压缩级别
          encryptionMethod: 'zip20', // 使用传统Zip 2.0加密，兼容性更好
          password: password // 设置密码
        });
        
        // 连接输出流
        archive.pipe(output);
        
        // 处理流关闭事件（完成归档）
        output.on('close', () => {
          console.log(`创建加密归档完成：${destPath}`);
          // 计算文件大小
          const stats = fs.statSync(destPath);
          // 返回结果
          resolve({
            path: destPath,
            size: stats.size,
            iv: null // 传统加密不需要IV
          });
        });
        
        // 错误处理
        archive.on('error', (err) => {
          console.error('创建加密归档出错:', err);
          reject(new Error(`创建ZIP文件失败: ${err.message}`));
        });
        
        // 判断源路径是文件还是目录，采用不同的处理方式
        const stats = fs.statSync(sourcePath);
        if (stats.isDirectory()) {
          // 目录处理 - 添加整个目录到压缩包
          archive.directory(sourcePath, false);
        } else {
          // 单个文件处理 - 添加单个文件
          archive.file(sourcePath, { name: path.basename(sourcePath) });
        }
        
        // 完成归档
        archive.finalize();
        
      } catch (error) {
        console.error('加密归档过程出错:', error);
        reject(new Error(`备份过程出错: ${error.message}`));
      }
    });
  }
  
  /**
   * 生成随机密码并返回密码提示
   * 为每次备份创建唯一密码，并生成便于记忆的提示
   * @returns {Object} 包含密码和提示的对象
   */
  generatePassword(password) {
    // 生成8位随机密码（4字节的随机数转为16进制，得到8个字符）
    
    
    // 创建密码提示（仅显示首尾字符，不泄露完整密码）
    const hint = `首字符: ${password.charAt(0)}，末字符: ${password.charAt(password.length - 1)}`;
    
    return {  hint };
  }
  
  /**
   * 全量备份 - 优化大数据量处理
   * 执行完整数据库备份，适用于初次备份或周期性完整备份
   * 采用分片策略处理大体积学生试卷数据
   * @returns {Promise<Object>} 备份结果信息
   */
  async fullBackup(password) {
    // 创建备份记录，标记开始备份
    const backupId = await this._startBackupRecord('full');
    
    try {
      // 生成带东八区时间戳的备份文件夹名称
      const timestamp = moment.tz('Asia/Shanghai').format('YYYY-MM-DD_HH-mm-ss');
      const backupFolder = path.join(this.fullBackupDir, `full_backup_${timestamp}`);
      fs.ensureDirSync(backupFolder); // 创建临时备份文件夹
      
      // 生成备份加密密码
      const {  hint } = this.generatePassword(password);
      
      // 1. 备份系统表和基础数据（小体积）
      const systemTables = [
        'role', 'user', 'classes', 'subject', 'student_classes', 
        'teacher_classes_subject', 'examination', 'answer_type', 
        'question_type', 'big_question', 'knowledge_point', 
        'question', 'subjective_answers', 'objective_answers', 
        'exam_plan', 'exam_plan_details',
        // 添加之前缺少的表
        'review_assignment', 'reminder_log', 'reminder_setting',
        'recheck', 'recheck_detail', 'recheck_result',
        'system_notification', 'notification_type'
      ];
      
      // 使用mysqldump导出系统表数据
      await mysqldump({
        connection: {
          host: process.env.DATABASE_HOST,
          user: process.env.DATABASE_USER,
          password: process.env.DATABASE_PASSWORD,
          database: process.env.DATABASE_NAME,
        },
        dumpToFile: path.join(backupFolder, 'system_tables.sql'), // 系统表备份文件
        tables: systemTables // 指定要备份的表
      });
      
      // 2. 分片备份大体积学生试卷数据
      // 获取学生试卷总数，用于确定分片数量
      const totalPapers = await prisma.student_paper.count();
      const totalSlices = Math.ceil(totalPapers / this.sliceSize); // 计算需要的分片数
      
      // 分批处理学生试卷和相关表
      for (let slice = 0; slice < totalSlices; slice++) {
        // 获取当前分片的学生试卷ID范围
        const paperIds = await prisma.student_paper.findMany({
          select: { id: true },
          skip: slice * this.sliceSize,   // 跳过前面已处理的记录
          take: this.sliceSize,           // 获取当前分片的记录数
          orderBy: { id: 'asc' }          // 按ID升序排序确保顺序一致
        });
        
        // 提取ID列表
        const paperIdList = paperIds.map(p => p.id);
        
        // 如果当前分片没有数据，跳过
        if (paperIdList.length === 0) continue;
        
        // 为每个分片创建专用SQL文件
        const sliceFile = path.join(backupFolder, `papers_slice_${slice + 1}.sql`);
        
        // 导出当前分片的学生试卷及相关数据
        await this._dumpPaperSlice(paperIdList, sliceFile);
      }
      
      // 3. 打包并加密整个备份文件夹
      const finalZipFile = `${backupFolder}.zip`; // 最终加密备份文件路径
      const archiveResult = await this.createEncryptedArchive(backupFolder, finalZipFile, password);
      
      // 备份完成后删除临时文件夹，只保留加密的zip文件
      try {
        await this._removeDirectoryRecursive(backupFolder);
      } catch (err) {
        console.warn('删除临时文件夹失败:', err);
      }
      
      // 计算备份文件大小
      const stats = fs.statSync(finalZipFile);
      const fileSizeInMB = (stats.size / (1024 * 1024)).toFixed(2); // 转换为MB并保留两位小数
      
      // 更新备份记录，标记备份完成
      await this._completeBackupRecord(backupId, {
        filename: path.basename(finalZipFile), // 文件名
        backup_path: finalZipFile,             // 完整路径
        encrypt_iv: archiveResult.iv,          // 加密IV
        backup_size: `${fileSizeInMB} MB`,     // 文件大小
        compress_type: 'encrypted',            // 压缩类型
        password_hint: hint,                   // 密码提示
        slice_count: totalSlices,              // 分片数量
        status: 'success',                     // 备份状态
        description: `加密全量备份，共${totalPapers}份试卷，分${totalSlices}个分片`, // 描述
        updated_at: new Date(new Date().getTime() + (8 * 60 * 60 * 1000)) // 明确设置东八区时间
      });
      
      console.log(`全量备份完成: ${finalZipFile}`); // 输出备份完成信息
      
      // 返回备份结果
      return {
        success: true,
        path: finalZipFile,
        size: `${fileSizeInMB} MB`,
        slices: totalSlices,
        passwordHint: hint
      };
    } catch (error) {
      // 备份过程出错处理
      console.error('全量备份失败:', error);
      
      // 更新备份记录为失败状态
      await this._completeBackupRecord(backupId, {
        status: 'failed',
        description: `备份失败: ${error.message}`
      });
      
      // 返回错误信息
      return {
        success: false,
        error: error.message
      };
    }
  }
  
  /**
   * 每日增量备份 - 只备份当天变更数据
   * 对上次备份后发生变化的数据进行备份，减少备份文件大小和执行时间
   * @returns {Promise<Object>} 备份结果信息
   */
  async incrementalBackup(password) {
    // 创建增量备份记录，标记开始备份
    const backupId = await this._startBackupRecord('incremental');
    let backupFolder; // 声明在外部以便错误处理时可以访问
    
    try {
      // 获取增量备份的时间范围（上次备份时间到现在）
      const { startTime, endTime, startTimeDate, endTimeDate } = await this.getIncrementalTimeRange();
      
      console.log('startTime: ', startTime);
      console.log('endTime: ', endTime);
 
      // 如果没有找到上次备份时间，无法执行增量备份
      if (!startTime) {
        // 更新备份记录为失败状态
        await this._completeBackupRecord(backupId, {
          status: 'failed',
          description: '没有找到之前的备份点，请先执行全量备份'
        });
        
        // 返回错误信息
        return {
          success: false,
          error: '没有找到之前的备份点，请先执行全量备份'
        };
      }
      
      // 统一使用东八区时间对象
      const beijingTime = moment.tz('Asia/Shanghai');
      const timestamp = beijingTime.format('YYYY-MM-DD_HH-mm-ss');
      backupFolder = path.join(this.incrementalBackupDir, `incremental_backup_${timestamp}`);
      fs.ensureDirSync(backupFolder); // 创建临时备份文件夹

      // 构建时间过滤条件
      const updatedTimeFilter = `updated_at >= '${startTime}' AND updated_at <= '${endTime}'`;
   
      console.log("startTime: ", startTime);
      console.log("endTime: ", endTime);
      console.log("startTimeDate: ", startTimeDate);
      console.log("endTimeDate: ", endTimeDate);
      console.log(`SQL筛选条件: ${updatedTimeFilter}`);
      
      // 生成备份加密密码
      const { hint } = this.generatePassword(password);
      
      // 记录备份数据范围描述（起止时间）
      const rangeDescription = `${startTime} 至 ${endTime}`;
      console.log('rangeDescription: ', rangeDescription);

      // 1. 按数据库表结构分组备份不同类型的表
      
      // 1.1 基础数据表
      const baseTables = [
        'role', 'user', 'classes', 'subject', 'student_classes', 
        'teacher_classes_subject', 'notification_type', 'reminder_setting'
      ];
      
      // 1.2 考试相关核心表
      const examTables = [
        'examination', 'exam_plan', 'exam_plan_details'
      ];
      
      // 1.3 试题相关表
      const questionTables = [
        'answer_type', 'question_type', 'big_question', 'knowledge_point',
        'question', 'subjective_answers', 'objective_answers'
      ];
      
      // 1.4 批阅相关表
      const reviewTables = [
        'review_assignment', 'review_assignment_detail'
      ];
      
      // 1.5 查卷复审相关表
      const recheckTables = [
        'recheck_request', 'recheck', 'recheck_detail', 'recheck_result'
      ];
      
      // 1.6 通知消息相关表
      const notificationTables = [
        'system_notification', 'reminder_log'
      ];
      
      // 1.7 学生试卷相关表
      const paperTables = [
        'student_paper', 'paper_detail'
      ];
      
      // 合并所有需要备份的表
      const allTablesToBackup = [
        ...baseTables, 
        ...examTables, 
        ...questionTables, 
        ...reviewTables, 
        ...recheckTables, 
        ...notificationTables,
        ...paperTables
      ];
      
      // 构建各表的筛选条件，确保每个表都应用时间过滤
      const tableConditions = {};
      allTablesToBackup.forEach(table => {
        tableConditions[table] = updatedTimeFilter;
      });
      
      console.log(`增量备份: 开始备份 ${allTablesToBackup.length} 个表的变更数据`);
      
      // 导出变更的表数据 - 使用 mysqldump 命令行工具直接执行，以确保过滤条件正确应用
      const dbConfig = {
        host: process.env.DATABASE_HOST,
        user: process.env.DATABASE_USER,
        password: process.env.DATABASE_PASSWORD,
        database: process.env.DATABASE_NAME
      };
      
     
      
      // 使用 mysqldump 命令行直接执行，确保过滤条件正确应用
      const { execSync } = require('child_process');
      const outputFile = path.join(backupFolder, 'modified_tables.sql');
      
      // 为每个表执行单独的导出，确保过滤条件应用
      let combinedSql = '';
      for (const table of allTablesToBackup) {
        try {
          const tableSqlFile = path.join(backupFolder, `${table}.sql`);
          
          // 构建mysqldump命令
          const dumpCmd = `mysqldump --host=${dbConfig.host} --user=${dbConfig.user} --password=${dbConfig.password} ` + 
                         `--no-create-info --skip-triggers --where="${tableConditions[table]}" ` +
                         `${dbConfig.database} ${table} > "${tableSqlFile}"`;
          
          console.log(`备份表 ${table} 中...`);
          execSync(dumpCmd, { stdio: 'pipe' });
          
          // 读取表SQL并合并
          if (fs.existsSync(tableSqlFile)) {
            const tableSql = fs.readFileSync(tableSqlFile, 'utf8');
            combinedSql += tableSql + '\n\n';
            // 删除单表SQL文件
            fs.unlinkSync(tableSqlFile);
          }
        } catch (err) {
          console.error(`表 ${table} 备份失败: ${err.message}`);
        }
      }
      
      // 写入合并后的SQL文件
      fs.writeFileSync(outputFile, combinedSql);
      
      console.log(`增量备份: 已完成所有表数据备份`);
      
      // 记录备份中涉及到的试卷数量统计
      const totalPapers = await prisma.student_paper.count({
        where: {
          updated_at: { 
            gte: new Date(startTime),
            lte: new Date(endTime)
          }
        }
      });
      
      console.log(`增量备份: 本次备份共包含 ${totalPapers} 份变更试卷`);
      
      // 3. 打包并加密整个备份文件夹
      console.log(`增量备份: 开始压缩加密备份数据`);
      const finalZipFile = `${backupFolder}.zip`; // 最终加密备份文件路径
      const archiveResult = await this.createEncryptedArchive(backupFolder, finalZipFile, password);
      
      // 备份完成后删除临时文件夹，只保留加密的zip文件
      try {
        await this._removeDirectoryRecursive(backupFolder);
      } catch (err) {
        console.warn('删除临时文件夹失败:', err);
      }
      
      // 计算备份文件大小
      const stats = fs.statSync(finalZipFile);
      const fileSizeInMB = (stats.size / (1024 * 1024)).toFixed(2); // 转换为MB并保留两位小数
      
      // 更新备份记录，标记备份完成
      await this._completeBackupRecord(backupId, {
        filename: path.basename(finalZipFile), // 文件名
        backup_path: finalZipFile,             // 完整路径
        encrypt_iv: archiveResult.iv,          // 加密IV
        backup_size: `${fileSizeInMB} MB`,     // 文件大小
        compress_type: 'encrypted',            // 压缩类型
        password_hint: hint,                   // 密码提示
        data_range: rangeDescription,          // 数据时间范围
        slice_count: Math.ceil(totalPapers / this.sliceSize), // 分片数量
        status: 'success',                     // 备份状态
        description: `加密增量备份，${rangeDescription}期间的变更数据`, // 描述
      });
      
      console.log(`增量备份完成: ${finalZipFile}`); // 输出备份完成信息
      
      // 返回备份结果
      return {
        success: true,
        path: finalZipFile,
        size: `${fileSizeInMB} MB`,
        range: rangeDescription,
        paperCount: totalPapers,
        tableCount: allTablesToBackup.length,
        passwordHint: hint
      };
    } catch (error) {
      // 增量备份过程出错处理
      console.error('增量备份失败:', error);
      
      // 更新备份记录为失败状态
      await this._completeBackupRecord(backupId, {
        status: 'failed',
        description: `增量备份失败: ${error.message}`
      });
      
      // 返回错误信息
      return {
        success: false,
        error: error.message
      };
    }
  }
  
  /**
   * 导出指定ID范围的学生试卷及相关数据
   * 将指定试卷ID列表相关的所有数据导出为SQL文件
   * 包括试卷、试卷详情、批阅任务详情、查卷申请和复审数据
   * @param {Array<number>} paperIds 试卷ID数组
   * @param {string} outputFile 输出SQL文件路径
   * @returns {Promise<boolean>} 操作成功返回true
   */
  async _dumpPaperSlice(paperIds, outputFile) {
    // 验证参数有效性
    if (!paperIds || paperIds.length === 0) return;
    
    // 创建临时SQL文件，使用.tmp后缀避免因中断操作导致文件损坏
    const tempSqlFile = outputFile + '.tmp';
    const writeStream = fs.createWriteStream(tempSqlFile);
    
    try {
      // 添加SQL文件头部注释，记录导出时间和数据范围
      writeStream.write(`-- 导出时间: ${new Date().toISOString()}\n`);
      writeStream.write(`-- 试卷ID范围: ${Math.min(...paperIds)} - ${Math.max(...paperIds)}\n\n`);
      
      // 1. 导出学生试卷表数据
      const paperData = await prisma.student_paper.findMany({
        where: { id: { in: paperIds } } // 筛选指定ID的试卷
      });
      
      // 如果找到试卷数据，生成INSERT语句
      if (paperData.length > 0) {
        writeStream.write(`-- 学生试卷表数据\n`);
        writeStream.write(`INSERT INTO student_paper VALUES\n`);
        
        // 逐条生成试卷数据的SQL值部分
        paperData.forEach((paper, index) => {
          const values = this._formatSqlValues(paper);
          // 最后一条不加逗号，否则加逗号分隔
          writeStream.write(`${values}${index < paperData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
      }
      
      // 2. 导出试卷详情表数据
      const detailData = await prisma.paper_detail.findMany({
        where: { student_paper_id: { in: paperIds } } // 筛选关联的试卷详情
      });
      
      // 如果找到试卷详情数据，生成INSERT语句
      if (detailData.length > 0) {
        writeStream.write(`-- 试卷详情表数据\n`);
        writeStream.write(`INSERT INTO paper_detail VALUES\n`);
        
        // 逐条生成试卷详情数据的SQL值部分
        detailData.forEach((detail, index) => {
          const values = this._formatSqlValues(detail);
          writeStream.write(`${values}${index < detailData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
        
        // 保存所有试卷详情ID，用于后续查询复审详情
        const paperDetailIds = detailData.map(d => d.id);
      }
      
      // 3. 导出批阅任务详情数据
      const reviewDetailData = await prisma.review_assignment_detail.findMany({
        where: { student_paper_id: { in: paperIds } } // 筛选关联的批阅任务详情
      });
      
      // 如果找到批阅任务详情数据，生成INSERT语句
      if (reviewDetailData.length > 0) {
        writeStream.write(`-- 批阅任务详情表数据\n`);
        writeStream.write(`INSERT INTO review_assignment_detail VALUES\n`);
        
        // 逐条生成批阅任务详情的SQL值部分
        reviewDetailData.forEach((detail, index) => {
          const values = this._formatSqlValues(detail);
          writeStream.write(`${values}${index < reviewDetailData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
      }
      
      // 4. 导出查卷申请数据
      const recheckRequestData = await prisma.recheck_request.findMany({
        where: { student_paper_id: { in: paperIds } } // 筛选关联的查卷申请
      });
      
      // 如果找到查卷申请数据，生成INSERT语句
      if (recheckRequestData.length > 0) {
        writeStream.write(`-- 查卷申请表数据\n`);
        writeStream.write(`INSERT INTO recheck_request VALUES\n`);
        
        // 逐条生成查卷申请数据的SQL值部分
        recheckRequestData.forEach((request, index) => {
          const values = this._formatSqlValues(request);
          writeStream.write(`${values}${index < recheckRequestData.length - 1 ? ',' : ';'}\n`);
        });
        
        writeStream.write(`\n`); // 添加空行分隔
        
        // 保存所有查卷申请ID，用于后续查询复审表
        const recheckRequestIds = recheckRequestData.map(r => r.id);
        
        // 5. 导出复审表数据
        if (recheckRequestIds.length > 0) {
          const recheckData = await prisma.recheck.findMany({
            where: { recheck_request_id: { in: recheckRequestIds } } // 筛选关联的复审记录
          });
          
          if (recheckData.length > 0) {
            writeStream.write(`-- 复审表数据\n`);
            writeStream.write(`INSERT INTO recheck VALUES\n`);
            
            recheckData.forEach((recheck, index) => {
              const values = this._formatSqlValues(recheck);
              writeStream.write(`${values}${index < recheckData.length - 1 ? ',' : ';'}\n`);
            });
            
            writeStream.write(`\n`); // 添加空行分隔
            
            // 保存所有复审ID，用于后续查询复审详情
            const recheckIds = recheckData.map(r => r.id);
            
            // 6. 导出复审详情表数据
            if (recheckIds.length > 0) {
              const recheckDetailData = await prisma.recheck_detail.findMany({
                where: { 
                  OR: [
                    { recheck_id: { in: recheckIds } },       // 关联到复审表
                    { student_paper_id: { in: paperIds } }    // 或直接关联到学生试卷
                  ]
                }
              });
              
              if (recheckDetailData.length > 0) {
                writeStream.write(`-- 复审详情表数据\n`);
                writeStream.write(`INSERT INTO recheck_detail VALUES\n`);
                
                recheckDetailData.forEach((detail, index) => {
                  const values = this._formatSqlValues(detail);
                  writeStream.write(`${values}${index < recheckDetailData.length - 1 ? ',' : ';'}\n`);
                });
                
                writeStream.write(`\n`); // 添加空行分隔
              }
            }
            
            // 7. 导出查卷结果表数据
            const recheckResultData = await prisma.recheck_result.findMany({
              where: { recheck_request_id: { in: recheckRequestIds } } // 筛选关联的查卷结果
            });
            
            if (recheckResultData.length > 0) {
              writeStream.write(`-- 查卷结果表数据\n`);
              writeStream.write(`INSERT INTO recheck_result VALUES\n`);
              
              recheckResultData.forEach((result, index) => {
                const values = this._formatSqlValues(result);
                writeStream.write(`${values}${index < recheckResultData.length - 1 ? ',' : ';'}\n`);
              });
              
              writeStream.write(`\n`); // 添加空行分隔
            }
          }
        }
      }
      
      // 关闭写入流，确保所有数据写入文件
      writeStream.end();
      
      // 完成后重命名临时文件为最终文件名，确保文件完整性
      await fs.rename(tempSqlFile, outputFile);
      
      return true; // 操作成功
    } catch (error) {
      // 导出过程出错处理
      console.error('导出试卷分片失败:', error);
      
      // 关闭写入流
      writeStream.end();
      
      // 清理临时文件，避免留下损坏的文件
      if (fs.existsSync(tempSqlFile)) {
        fs.unlinkSync(tempSqlFile);
      }
      
      // 抛出错误，由上层函数处理
      throw error;
    }
  }
  
  /**
   * 将对象格式化为SQL值字符串
   * 处理不同数据类型，确保生成有效的SQL值语法
   * @param {Object} obj 包含字段值的对象
   * @returns {string} 格式化后的SQL VALUES语句部分
   */
  _formatSqlValues(obj) {
    // 将对象的值转换为SQL兼容的字符串表示
    const values = Object.values(obj).map(value => {
      if (value === null) return 'NULL'; // NULL值处理
      if (value instanceof Date) {
        // 将Date对象转换为东八区(中国标准时间)的日期时间字符串
        return `'${moment(value).tz('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss')}'`;
      }
      if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; // 字符串处理，转义单引号
      return value; // 其他类型直接返回
    });
    
    // 组合为(value1, value2, ...)格式
    return `(${values.join(', ')})`;
  }
  
  /**
   * 开始一个备份记录
   * 在数据库中创建备份任务记录，标记备份开始
   * @param {string} backupType 备份类型（'full', 'incremental', 'differential'）
   * @returns {Promise<number>} 新创建的备份记录ID
   */
  async _startBackupRecord(backupType) {
    // 获取东八区(中国标准时间)的当前时间
   
    const now = new Date();
    const utc8Date = new Date(now.getTime() + (8 * 60 * 60 * 1000));

    // 在数据库中创建一条备份记录，标记为进行中状态
    const record = await prisma.backup_record.create({
      data: {
        filename: 'pending',          // 临时文件名，将在完成后更新
        backup_path: 'pending',       // 临时路径，将在完成后更新
        backup_type: backupType,      // 备份类型
        start_time: utc8Date,      // 备份开始时间（东八区）
        end_time: utc8Date,        // 临时结束时间（东八区）
        status: 'in_progress',        // 状态：进行中
        compress_type: 'encrypted',   // 添加压缩类型字段
        description: `${backupType} 备份进行中...`, // 初始描述
      }
    });
    
    return record.id; // 返回新创建记录的ID
  }
  
  /**
   * 完成一个备份记录
   * 更新数据库中的备份记录，标记备份完成或失败
   * @param {number} backupId 备份记录ID
   * @param {Object} data 要更新的数据
   * @returns {Promise<void>}
   */
  async _completeBackupRecord(backupId, data) {
    // 获取东八区(中国标准时间)的当前时间
  
    const now = new Date();
    const utc8Date = new Date(now.getTime() + (8 * 60 * 60 * 1000));
    // 更新备份记录，包含最终状态和结束时间
    const updateData = {
      ...data,                // 更新传入的所有数据
      end_time: utc8Date,   // 设置备份结束时间（东八区）
      created_at: utc8Date
    };
    
    // 如果是使用标准加密ZIP，不需要encrypt_iv字段
    if (updateData.encrypt_iv === null) {
      delete updateData.encrypt_iv;
    }
    
    await prisma.backup_record.update({
      where: { id: backupId },
      data: updateData
    });
  }
  
  /**
   * 清理过期备份
   * 删除指定天数前的备份文件和数据库记录，但保留最近的全量备份
   * @param {number} days 保留的天数，默认30天
   * @returns {Promise<Object>} 清理结果信息
   */
  async cleanOldBackups(days = 30) {
    try {
      // 计算截止日期（当前日期减去指定天数）
      const cutoffDate = moment().tz('Asia/Shanghai').subtract(days, 'days').toDate();
      
      // 找到最近的成功全量备份，这个备份将被保留
      const lastFullBackup = await prisma.backup_record.findFirst({
        where: {
          backup_type: 'full',
          status: 'success'
        },
        orderBy: {
          created_at: 'desc'
        }
      });
      
      // 查询过期的备份记录
      const oldBackups = await prisma.backup_record.findMany({
        where: {
          created_at: {
            lt: cutoffDate // 早于截止日期的记录
          },
          status: 'success',
          // 排除最近的全量备份（如果存在）
          NOT: lastFullBackup ? {
            id: lastFullBackup.id
          } : undefined
        }
      });
      
      // 删除文件并更新数据库
      for (const backup of oldBackups) {
        // 删除备份文件
        if (fs.existsSync(backup.backup_path)) {
          fs.unlinkSync(backup.backup_path);
        }
        
        // 删除数据库记录
        await prisma.backup_record.delete({
          where: { id: backup.id }
        });
      }
      
      console.log(`已清理 ${oldBackups.length} 个过期备份`);
      return {
        success: true,
        count: oldBackups.length // 返回清理的备份数量
      };
    } catch (error) {
      console.error('清理过期备份失败:', error);
      return {
        success: false,
        error: error.message
      };
    }
  }
  
  /**
   * 获取备份列表
   * 查询数据库中的备份记录，支持分页和按类型筛选
   * @param {string|null} type 备份类型筛选，null表示所有类型
   * @param {number} limit 每页记录数，默认50
   * @param {number} offset 跳过的记录数，用于分页
   * @returns {Promise<Object>} 备份列表和总数
   */
  async getBackupList(type = null, limit = 50, offset = 0) {
    try {
      // 构建查询条件，如果指定了类型则按类型筛选
      const where = type ? { backup_type: type } : {};
      
      // 查询备份记录，按创建时间倒序排列
      const backups = await prisma.backup_record.findMany({
        where,
        orderBy: {
          created_at: 'desc' // 最新的备份排在前面
        },
        skip: offset,        // 分页参数：跳过记录数
        take: limit          // 分页参数：获取记录数
      });
      
      // 获取符合条件的总记录数
      const count = await prisma.backup_record.count({ where });
      
      // 返回查询结果
      return {
        success: true,
        data: backups,  // 备份记录列表
        total: count    // 总记录数
      };
    } catch (error) {
      console.error('获取备份列表失败:', error);
      return {
        success: false,
        error: error.message
      };
    }
  }


/**
 * 还原上传的备份数据到数据库
 * @param {Object} file 从前端上传的文件对象
 * @param {string} password 解密备份文件的密码
 * @param {Object} options 还原选项，包含处理冲突的策略
 * @returns {Promise<Object>} 还原结果信息
 */
async restoreBackup(file, password, options = {}) {
  // 默认选项设置
  const defaultOptions = {
    conflictStrategy: 'replace',
    backupBeforeRestore: true,
    ignoreErrors: false
  };
  
  const restoreOptions = {...defaultOptions, ...options};
  const restoreId = await this._startBackupRecord('restore');
  
  // 处理解压和恢复的临时目录与文件
  const timestamp = moment.tz('Asia/Shanghai').format('YYYY-MM-DD_HH-mm-ss');
  const tempDir = path.join(this.backupDir, `temp_${timestamp}`);
  const extractDir = path.join(tempDir, 'extract');
  const uploadedFilePath = path.join(tempDir, 'uploaded_backup.zip');
  
  let fileSizeInMB = '未知';
  let importedFiles = 0;
  
  try {
    console.log('开始处理备份还原请求...');
    console.log('文件信息:', {
      hasFile: !!file,
      hasBuffer: !!(file && file.buffer),
      hasData: !!(file && file.data),
      originalname: file && file.originalname,
      mimetype: file && file.mimetype
    });
    
    // 1. 详细的文件验证
    if (!file) {
      throw new Error('未提供备份文件，请确保已选择文件');
    }
    
    // 验证文件数据
    const fileData = file.buffer || file.data;
 
    if (!fileData) {
      throw new Error('备份文件数据无效，请重新上传');
    }
    
    // 验证文件大小
    const fileSize = fileData.length;
    if (fileSize === 0) {
      throw new Error('备份文件大小为0字节，请检查文件是否正确上传');
    }
    
    fileSizeInMB = (fileSize / (1024 * 1024)).toFixed(2);
    console.log(`备份文件大小: ${fileSizeInMB} MB`);
    
    // 验证文件名和MIME类型
    if (!file.originalname) {
      file.originalname = `backup_${timestamp}.zip`;
      console.log('文件名未提供，使用默认文件名:', file.originalname);
    }
    
    const fileExt = path.extname(file.originalname).toLowerCase();
    if (fileExt !== '.zip') {
      throw new Error(`不支持的文件格式: ${fileExt}，只支持.zip格式的备份文件`);
    }
    
    // 验证MIME类型
    const validMimeTypes = ['application/zip', 'application/x-zip-compressed', 'application/octet-stream'];
    if (file.mimetype && !validMimeTypes.includes(file.mimetype)) {
      console.warn(`警告: 文件MIME类型 ${file.mimetype} 可能不是有效的ZIP文件`);
    }
    
    // 2. 创建临时目录
    try {
      await fs.ensureDir(tempDir);
      await fs.ensureDir(extractDir);
      console.log('临时目录创建成功:', {
        tempDir,
        extractDir
      });
    } catch (dirError) {
      throw new Error(`创建临时目录失败: ${dirError.message}`);
    }
    
    // 3. 保存上传的文件
    try {
      console.log('开始写入备份文件...');
      await fs.writeFile(uploadedFilePath, fileData);
      
      // 验证文件是否成功写入
      const stats = await fs.stat(uploadedFilePath);
      if (stats.size === 0) {
        throw new Error('文件写入失败，生成的文件为空');
      }
      
      if (stats.size !== fileSize) {
        throw new Error(`文件写入不完整: 预期 ${fileSize} 字节，实际写入 ${stats.size} 字节`);
      }
      
      console.log(`备份文件已成功保存到: ${uploadedFilePath}`);
     
    } catch (writeError) {
      throw new Error(`保存备份文件失败: ${writeError.message}`);
    }
    
    // 4. 验证ZIP文件完整性
    try {
      console.log('开始验证ZIP文件完整性...');
      const AdmZip = require('adm-zip');
      const zip = new AdmZip(uploadedFilePath);
      
      // 尝试读取ZIP文件头
      const zipEntries = zip.getEntries();
      console.log(`ZIP文件包含 ${zipEntries.length} 个条目`);
      
   
      
    
   
      
    } catch (zipError) {
      if (zipError.message.includes('Invalid or unsupported')) {
        throw new Error('备份文件格式无效或已损坏，请确保文件未被修改');
      }
      throw new Error(`验证备份文件失败: ${zipError.message}`);
    }

    // 5. 解密并解压备份文件
    let sqlFiles = [];
    try {
      console.log('开始解密和解压备份文件...');
      
      // 首先尝试使用AdmZip直接解压
      const AdmZip = require('adm-zip');
      const zip = new AdmZip(uploadedFilePath);
      const zipEntries = zip.getEntries();
      
      // 检查是否是加密的zip文件
      const isEncrypted = zipEntries.some(entry => entry.isEncrypted);
      console.log("isEncrypted: ",isEncrypted);
      if (password) {
        console.log('尝试用密码解压（即使文件未标记为加密）');
        zip.extractAllTo(extractDir, true, false, password);
      } else {
        console.log('未提供密码，尝试直接解压');
        zip.extractAllTo(extractDir, true);
      }
      
      // 验证解压结果
      const extractedFiles = fs.readdirSync(extractDir);
      console.log(`解压出 ${extractedFiles.length} 个文件`);
      
      if (extractedFiles.length === 0) {
        throw new Error('解压后没有文件，可能密码错误或备份文件已损坏');
      }
      
      // 验证解压出的文件
       sqlFiles = extractedFiles.filter(file => file.toLowerCase().endsWith('.sql'));
      if (sqlFiles.length === 0) {
        throw new Error('解压后未找到SQL文件，备份文件可能已损坏');
      }
      
      // 检查SQL文件的有效性
      for (const sqlFile of sqlFiles) {
        const sqlFilePath = path.join(extractDir, sqlFile);
        const sqlContent = await fs.readFile(sqlFilePath, 'utf8');
        
        if (!sqlContent || sqlContent.trim().length === 0) {
          throw new Error(`SQL文件 ${sqlFile} 为空`);
        }
        
        // 简单验证SQL文件格式
        if (!sqlContent.toLowerCase().includes('insert into') && 
            !sqlContent.toLowerCase().includes('create table')) {
          throw new Error(`SQL文件 ${sqlFile} 不包含有效的SQL语句`);
        }
      }
      
      console.log('备份文件解密和验证成功，准备开始还原数据...');
      
    } catch (error) {
      throw new Error(`密码错误，无法解压!`);
    }

    // 6. 如果选择还原前备份，先进行一次备份
    if (restoreOptions.backupBeforeRestore) {
      try {
        console.log('在还原前进行数据备份...');
        const backupResult = await this.fullBackup(password);
        if (!backupResult.success) {
          throw new Error(backupResult.error || '备份失败');
        }
        console.log(`还原前的备份已完成: ${backupResult.path}`);
      } catch (backupError) {
        throw new Error(`还原前的备份失败: ${backupError.message}`);
      }
    }

    // 继续执行后续的还原步骤...

    // 7. 根据还原策略处理SQL文件
    await this._preprocessSqlFiles(extractDir, restoreOptions.conflictStrategy);
    
    // 8. 获取数据库连接信息
    const dbConfig = {
      host: process.env.DATABASE_HOST,
      user: process.env.DATABASE_USER,
      password: process.env.DATABASE_PASSWORD,
      database: process.env.DATABASE_NAME
    };
    
    console.log(`准备使用 ${restoreOptions.conflictStrategy} 策略还原数据到数据库: ${dbConfig.database}`);
    
    // 9. 使用子进程执行mysql命令导入SQL文件
    const { execSync } = require('child_process');
    
    // 按优先级排序SQL文件
    sqlFiles.sort((a, b) => {
      // 优先级排序：系统表 > 变更表 > 分片文件
      if (a === 'system_tables.sql') return -1;
      if (b === 'system_tables.sql') return 1;
      if (a === 'modified_tables.sql') return -1;
      if (b === 'modified_tables.sql') return 1;
      if (a.includes('papers_slice_') && !b.includes('papers_slice_')) return 1;
      if (!a.includes('papers_slice_') && b.includes('papers_slice_')) return -1;
      return a.localeCompare(b); // 保持稳定排序
    });
    
    // 10. 执行SQL文件导入
    console.log(`开始按顺序导入 ${sqlFiles.length} 个SQL文件...`);
    const importErrors = [];
    
    for (const sqlFile of sqlFiles) {
      const sqlFilePath = path.join(extractDir, sqlFile);
      console.log(`正在导入: ${sqlFile}`);
      
      try {
        // 构建mysql导入命令
        const importCmd = `mysql -h${dbConfig.host} -u${dbConfig.user} -p${dbConfig.password} ${dbConfig.database} < "${sqlFilePath}"`;
        
        // 执行导入命令
        execSync(importCmd, { stdio: 'pipe' });
        console.log(`- ${sqlFile} 导入成功`);
        importedFiles++;
      } catch (error) {
        console.error(`- ${sqlFile} 导入失败: ${error.message}`);
        importErrors.push(`${sqlFile}: ${error.message}`);
        
        // 如果不忽略错误，则终止还原过程
        if (!restoreOptions.ignoreErrors) {
          throw new Error(`SQL导入失败(${sqlFile}): ${error.message}`);
        }
      }
    }
    
    // 11. 记录上传的备份到数据库
    try {
      // 创建备份记录
      await prisma.backup_record.create({
        data: {
          filename: file.originalname,
          backup_path: uploadedFilePath,
          backup_type: 'upload', // 上传的备份类型
          backup_size: `${fileSizeInMB} MB`,
          compress_type: 'encrypted',
          password_hint: '用户上传的备份',
          status: 'restored', // 标记为已还原状态
          description: '用户上传并还原的备份文件',
          start_time: new Date(),
          end_time: new Date(),
          created_at: new Date(),
          updated_at: new Date()
        }
      });
      
      console.log(`已将上传的备份记录保存到数据库`);
    } catch (error) {
      console.warn('记录上传备份信息失败:', error);
    }
    
    // 12. 完成恢复，更新记录
    const description = importErrors.length > 0 
      ? `从上传文件(${file.originalname})恢复数据完成，但有${importErrors.length}个文件导入失败` 
      : `成功从上传文件(${file.originalname})恢复数据，使用策略: ${restoreOptions.conflictStrategy}`;
    
    await this._completeBackupRecord(restoreId, {
      status: importErrors.length > 0 && importedFiles < sqlFiles.length / 2 ? 'partial' : 'success',
      description: description,
      backup_size: `${fileSizeInMB} MB`
    });
    
    console.log(`数据恢复完成！共导入 ${importedFiles}/${sqlFiles.length} 个文件`);
    
    // 删除所有临时文件和目录
    try {
      console.log('清理所有临时目录...');
      if (fs.existsSync(tempDir)) {
        await fs.remove(tempDir);
        console.log(`- 已删除临时目录: ${tempDir}`);
      }
    } catch (cleanupError) {
      console.warn('清理临时目录失败:', cleanupError);
    }
    
    return {
      success: true,
      message: `成功从上传文件(${file.originalname})恢复数据`,
      strategy: restoreOptions.conflictStrategy,
      restoredFiles: importedFiles,
      totalFiles: sqlFiles.length,
      errors: importErrors.length > 0 ? importErrors : undefined
    };
  } catch (error) {
    console.error('数据恢复失败:', error);
    
    // 更新恢复记录为失败状态
    await this._completeBackupRecord(restoreId, {
      status: 'failed',
      description: `恢复失败: ${error.message}`,
      backup_size: `${fileSizeInMB} MB`
    });
    
    // 清理临时文件
    try {
      console.log('由于发生错误，清理所有临时文件...');
      if (fs.existsSync(tempDir)) {
        await fs.remove(tempDir);
        console.log(`- 已删除临时目录: ${tempDir}`);
      }
    } catch (cleanupError) {
      console.warn('清理临时文件失败:', cleanupError);
    }
    
    return {
      success: false,
      error: error.message,
      details: {
        fileSize: `${fileSizeInMB} MB`,
        originalName: file?.originalname,
        mimeType: file?.mimetype
      }
    };
  }
}

/**
 * 预处理SQL文件，根据冲突策略修改SQL语句
 * @param {string} extractDir 解压目录
 * @param {string} conflictStrategy 冲突处理策略
 * @returns {Promise<void>}
 */
async _preprocessSqlFiles(extractDir, conflictStrategy) {
  console.log(`使用 ${conflictStrategy} 策略预处理SQL文件...`);
  
  // 获取所有SQL文件
  const sqlFiles = fs.readdirSync(extractDir).filter(file => file.endsWith('.sql'));
  
  for (const sqlFile of sqlFiles) {
    const filePath = path.join(extractDir, sqlFile);
    let sqlContent = fs.readFileSync(filePath, 'utf8');
    let modified = false;
    
    // 根据冲突策略修改SQL内容
    switch (conflictStrategy) {
      case 'replace':
        // 替换 INSERT INTO 为 REPLACE INTO
        const originalContent = sqlContent;
        sqlContent = sqlContent.replace(/INSERT INTO/gi, 'REPLACE INTO');
        modified = originalContent !== sqlContent;
        break;
        
      case 'ignore':
        // 替换 INSERT INTO 为 INSERT IGNORE INTO
        const beforeIgnore = sqlContent;
        sqlContent = sqlContent.replace(/INSERT INTO/gi, 'INSERT IGNORE INTO');
        modified = beforeIgnore !== sqlContent;
        break;
        
      case 'truncate':
        // 提取表名，为每个表添加TRUNCATE语句
        const tableNames = new Set();
        
        // 查找所有INSERT语句的表名
        const tableRegex = /INSERT INTO\s+[`"]?(\w+)[`"]?/gi;
        let match;
        while ((match = tableRegex.exec(sqlContent)) !== null) {
          tableNames.add(match[1]);
        }
        
        if (tableNames.size > 0) {
          // 创建TRUNCATE语句
          const truncateStatements = Array.from(tableNames)
            .map(table => `TRUNCATE TABLE \`${table}\`;`)
            .join('\n');
          
          // 在文件开头添加TRUNCATE语句
          sqlContent = truncateStatements + '\n\n' + sqlContent;
          modified = true;
        }
        break;
    }
    
    // 如果内容被修改，写回文件
    if (modified) {
      fs.writeFileSync(filePath, sqlContent, 'utf8');
      console.log(`- 已处理 ${sqlFile}`);
    }
  }
  
  console.log(`SQL文件预处理完成`);
}

/**
 * 递归删除目录及其内容
 * @param {string} dirPath 要删除的目录路径
 */
async _removeDirectoryRecursive(dirPath) {
  try {
    if (!fs.existsSync(dirPath)) return;
    
    // 使用fs-extra的remove方法，它能够处理非空目录
    await fs.remove(dirPath);
    console.log(`成功删除目录: ${dirPath}`);
  } catch (error) {
    console.warn(`删除目录失败: ${dirPath}`, error);
    throw error;
  }
}

}

module.exports = new BackupService();